import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Vector;


public class Scanner {
	Vector<Token> tokens;
	Integer tokenIndex;
	
	public Scanner() {
		this.tokens = new Vector<Token>();
		tokenIndex = 0;
	}
	
	/**
	 * Splits the input by whitespace and a few other rules so that it can
	 * be tokenized.
	 * @param filename
	 * @throws IOException
	 */
	public void scan(String filename) throws IOException {
		// open the file
		BufferedReader in = new BufferedReader(new FileReader(filename));
		
		Integer lineNumber = 0;
		while (in.ready()) {
			String line = in.readLine().trim();
			// keep track of line number
			lineNumber++;
			// ignore empty lines
			if (line.length() <= 0) continue;
			Integer index = 0;
			String tokenSoFar = "";
			// lots of booleans for handling special cases like quotes and escaping
			boolean inSingleQuotes = false, inDoubleQuotes = false;
			boolean escape = false, lastWasEscape = false;
			// handle up to the last character in the line
			while (index < line.length()-1) {
				Character c = line.charAt(index);
				switch (c) {
				case '\\':
					escape = true;
					break;
				case '\'':
					if (!inDoubleQuotes && !lastWasEscape)
						inSingleQuotes = !inSingleQuotes;
					tokenSoFar += c;
					break;
				case '"':
					if (!inSingleQuotes && !lastWasEscape)
						inDoubleQuotes = !inDoubleQuotes;
					tokenSoFar += c;
					break;
				case '#':
					if (!inSingleQuotes && !inDoubleQuotes) {
						tokenSoFar = c.toString();
						addToken(tokenSoFar, lineNumber, line);
						tokenSoFar = "";
					} else {
						tokenSoFar += c;
					}
					break;
				case '(':
					if (!inSingleQuotes && !inDoubleQuotes) {
						if (tokenSoFar.length() > 0) addToken(tokenSoFar, lineNumber, line);
						tokenSoFar = c.toString();
						addToken(tokenSoFar, lineNumber, line);
						tokenSoFar = "";
					} else {
						tokenSoFar += c;
					}
					break;
				case ')':
					if (!inSingleQuotes && !inDoubleQuotes) {
						addToken(tokenSoFar, lineNumber, line);
						tokenSoFar = c.toString();
						addToken(tokenSoFar, lineNumber, line);
						tokenSoFar = "";
					} else {
						tokenSoFar += c;
					}
					break;
				case ' ':
					if (!inSingleQuotes && !inDoubleQuotes) {
						addToken(tokenSoFar, lineNumber, line);
						tokenSoFar = "";
					} else {
						tokenSoFar += c;
					}
					break;
				default:
					tokenSoFar += c;
				}
				index++;
				lastWasEscape = escape;
				escape = false;
			}
			// handle last character
			switch (line.charAt(index)) {
			case ';':
				addToken(tokenSoFar, lineNumber, line);
				addToken(";", lineNumber, line);
				break;
			case ' ':
				addToken(tokenSoFar, lineNumber, line);
				break;
			default:
				tokenSoFar += line.charAt(index);
				addToken(tokenSoFar, lineNumber, line);
			}
		}
		in.close();
	}
	
	/**
	 * Handles converting a string to a token and recording it in the instance's
	 * vector
	 * @param s
	 * @param lineNumber
	 * @param line
	 */
	private void addToken(String s, int lineNumber, String line) {
		Token t = toToken(s, lineNumber, line);
		if (t != null) {
			tokens.add(t);
			System.out.println(t);
		}
	}
	
	/** 
	 * Returns a token based on a string
	 * @param s
	 * @param lineNumber
	 * @param line
	 * @return
	 */
	private Token toToken(String s, int lineNumber, String line) {
		Token t = new Token();
		t.setLine(line);
		t.setLineNumber(lineNumber);
		t.setLiteral(s);
		// figure out what this string is
		if (s.length() == 0)
			return null;
		else if (s.equals("begin"))
			t.setType(TokenClass.BEGIN);
		else if (s.equals("end"))
			t.setType(TokenClass.END);
		else if (s.equals("="))
			t.setType(TokenClass.EQUALS);
		else if (s.equals("with"))
			t.setType(TokenClass.WITH);
		else if (s.equals("in"))
			t.setType(TokenClass.IN);
		else if (s.equals("replace"))
			t.setType(TokenClass.REPLACE);
		else if (s.equals("recursivereplace"))
			t.setType(TokenClass.RECURSIVEREPLACE);
		else if (s.equals(";"))
			t.setType(TokenClass.SEMICOLON);
		else if (s.equals(">!"))
			t.setType(TokenClass.REDIRECT);
		else if (s.equals("print"))
			t.setType(TokenClass.PRINT);
		else if (s.equals("("))
			t.setType(TokenClass.OPENPAR);
		else if (s.equals(")"))
			t.setType(TokenClass.CLOSEPAR);
		else if (s.equals(","))
			t.setType(TokenClass.COMMA);
		else if (s.equals("#"))
			t.setType(TokenClass.HASH);
		else if (s.equals("find"))
			t.setType(TokenClass.FIND);
		else if (s.equals("diff"))
			t.setType(TokenClass.DIFF);
		else if (s.equals("union"))
			t.setType(TokenClass.UNION);
		else if (s.equals("inters"))
			t.setType(TokenClass.INTERS);
		else if (s.equals("maxfreqstring"))
			t.setType(TokenClass.MAXFREQSTR);
		else if (s.charAt(0) == '\'') {
			// strip quotes
			String lit = t.getLiteral();
			t.setLiteral(lit.substring(1, lit.length()-1));
			t.setType(TokenClass.REGEX);
		}
		else if (s.charAt(0) == '"') {
			// strip quotes
			String lit = t.getLiteral();
			t.setLiteral(lit.substring(1, lit.length()-1));
			t.setType(TokenClass.ASCIISTR);
		}
		else {
			// if it's nothing else, it must be an ID, so do some constraint checking
			boolean isValid = true;
			String reason = "";
			// make sure that it starts with a letter
			if (!Character.isLetter(s.charAt(0))) {
				isValid = false;
				reason = "first character is not a letter a-zA-Z";
			}
			// make sure it has reasonable length
			if (s.length() > 16) {
				isValid = false;
				reason = "string is over 16 characters long";
			}
			// ensure that every char is alphanumeric or underscore
			for (Character c : s.toCharArray()) {
				if (!Character.isLetter(c) && !Character.isDigit(c) && !c.equals('_')) {
					isValid = false;
					reason = "invalid character " + c;
				}
			}
			
			// if it's valid, set its ID type, otherwise throw an error 
			if (isValid)
				t.setType(TokenClass.ID);
			else {
				System.err.println("Syntax error! Could not match malformed token: " + s);
				System.err.println(reason);
				System.exit(0);
			}
		}
		return t;
	}
	
	/**
	 * Simulates an iterator action, grabs the current token and increments the counter
	 * @return
	 */
	public Token get(){
		if (tokenIndex+1 > tokens.size())
			return new Token(TokenClass.EMPTY,"",0,"");
		return tokens.get(tokenIndex++);
	}

	/**
	 * Like get(), but doesn't increment the counter
	 * @return
	 */
	public Token peek(){
		if (tokenIndex+1 > tokens.size())
			return new Token(TokenClass.EMPTY,"",0,"");
		return tokens.get(tokenIndex);
	}
	
	/**
	 * Simulates putting back a token
	 */
	public void replace(){
		tokenIndex--;
	}
	
	/**
	 * Useful for loops, gives the status of the iterator
	 * @return
	 */
	public boolean hasNextToken() {
		return tokenIndex + 1 <= tokens.size();
	}
	
	/**
	 * Resets the iterator like it hasn't been used
	 */
	public void reset() {
		tokenIndex = 0;
	}
	
	public static void main(String[] args) {
		Scanner s = new Scanner();
		try {
			s.scan(args[0]);
		} catch (IOException e) {
			System.out.println("nope, i/o error");
		}
	}
}
